/**
 * Copyright (c) 2005-2006, Sun Microsystems, Inc
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above
 *     copyright notice, this list of conditions and the following 
 *     disclaimer in the documentation and/or other materials provided 
 *     with the distribution.
 *   * Neither the name of the TimingFramework project nor the names of its
 *     contributors may be used to endorse or promote products derived 
 *     from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.jdesktop.animation.timing.interpolation;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTargetAdapter;

/**
 * This class enables automating animation of object properties. The class is a
 * TimingTarget, and should be used as a target of timing events from an
 * Animator. These events will be used to change a specified property over time,
 * according to how the PropertySetter is constructed.
 * <p>
 * For example, here is an animation of the "background" property of some object
 * "obj" from blue to red over a period of one second:
 * 
 * <pre>
 * PropertySetter ps = new PropertySetter(obj, &quot;background&quot;, Color.BLUE, Color.RED);
 * Animator anim = new Animator(1000, ps);
 * anim.start();
 * </pre>
 * 
 * Here is the same animation, created using one of the utility factory methods
 * that returns an animator:
 * 
 * <pre>
 * Animator animator = PropertySetter.createAnimator(1000, obj, &quot;background&quot;,
 * 		Color.BLUE, Color.RED);
 * anim.start();
 * </pre>
 * 
 * <p>
 * More complex animations can be created by passing in multiple values for the
 * property to take on, for example:
 * 
 * <pre>
 * Animator animator = PropertySetter.createAnimator(1000, obj, &quot;background&quot;,
 * 		Color.BLUE, Color.RED, Color.GREEN);
 * anim.start();
 * </pre>
 * 
 * It is also possible to define more involved and tightly-controlled steps in
 * the animation, including the times between the values and how the values are
 * interpolated by using the constructor that takes a {@link KeyFrames} object.
 * KeyFrames defines the fractional times at which an object takes on specific
 * values, the values to assume at those times, and the method of interpolation
 * between those values. For example, here is the same animation as above,
 * specified through KeyFrames, where the RED color will be set 10% of the way
 * through the animation (note that we are not setting an Interpolator, so the
 * timing intervals will use the default LinearInterpolator):
 * 
 * <pre>
 * KeyValues vals = KeyValues.create(Color.BLUE, Color.RED, Color.GREEN);
 * KeyTimes times = new KeyTimes(0.0f, .1f, 1.0f);
 * KeyFrames frames = new KeyFrames(vals, times);
 * Animator animator = PropertySetter.createAnimator(1000, obj, &quot;background&quot;,
 * 		frames);
 * anim.start();
 * </pre>
 * 
 * @author Chet
 */
public class PropertySetter extends TimingTargetAdapter {

	private Object object;
	private String propertyName;
	private KeyFrames keyFrames;
	private Method propertySetter;
	private Method propertyGetter;

	/**
	 * Utility method that constructs a PropertySetter and an Animator using
	 * that PropertySetter and returns the Animator
	 * 
	 * @param duration
	 *            the duration, in milliseconds, of the animation
	 * @param object
	 *            the object whose property will be animated
	 * @param propertyName
	 *            the name of the property to be animated. For any propertyName
	 *            "foo" there must be an accessible "setFoo" method on the
	 *            object. If only one value is supplied in creating the
	 *            KeyValues for the keyFrames, the animation will also need a
	 *            "getFoo" method.
	 * @param keyFrames
	 *            the fractional times, values, and interpolation to be used in
	 *            calculating the values set on the object's property.
	 * @throws IllegalArgumentException
	 *             if appropriate set/get methods cannot be found for
	 *             propertyName.
	 */
	public static Animator createAnimator(int duration, Object object,
			String propertyName, KeyFrames keyFrames) {
		PropertySetter ps = new PropertySetter(object, propertyName, keyFrames);
		Animator animator = new Animator(duration, ps);
		return animator;
	}

	/**
	 * Utility method that constructs a PropertySetter and an Animator using
	 * that PropertySetter and returns the Animator
	 * 
	 * @param duration
	 *            the duration, in milliseconds, of the animation
	 * @param object
	 *            the object whose property will be animated
	 * @param propertyName
	 *            the name of the property to be animated. For any propertyName
	 *            "foo" there must be an accessible "setFoo" method on the
	 *            object. If only one value is supplied in creating the
	 *            KeyValues for the keyFrames, the animation will also need a
	 *            "getFoo" method.
	 * @param params
	 *            the values that the object will take on during the animation.
	 *            Internally, a KeyFrames object will be created that will use
	 *            times that split the total duration evenly. Supplying only one
	 *            value for params implies that this is a "to" animation whose
	 *            intial value will be determined dynamically when the animation
	 *            starts.
	 * @throws IllegalArgumentException
	 *             if appropriate set/get methods cannot be found for
	 *             propertyName.
	 */
	public static <T> Animator createAnimator(int duration, Object object,
			String propertyName, T... params) {
		PropertySetter ps = new PropertySetter(object, propertyName, params);
		Animator animator = new Animator(duration, ps);
		return animator;
	}

	/**
	 * Utility method that constructs a PropertySetter and an Animator using
	 * that PropertySetter and returns the Animator
	 * 
	 * @param duration
	 *            the duration, in milliseconds, of the animation
	 * @param object
	 *            the object whose property will be animated
	 * @param propertyName
	 *            the name of the property to be animated. For any propertyName
	 *            "foo" there must be an accessible "setFoo" method on the
	 *            object. If only one value is supplied in creating the
	 *            KeyValues for the keyFrames, the animation will also need a
	 *            "getFoo" method.
	 * @param evaluator
	 *            KeyValues knows how to calculate intermediate values for many
	 *            built-in types, but if you want to supply values in types not
	 *            understood by KeyValues, you will need to supply your own
	 *            Evaluator.
	 * @param params
	 *            the values that the object will take on during the animation.
	 *            Internally, a KeyFrames object will be created that will use
	 *            times that split the total duration evenly. Supplying only one
	 *            value for params implies that this is a "to" animation whose
	 *            intial value will be determined dynamically when the animation
	 *            starts.
	 * @throws IllegalArgumentException
	 *             if appropriate set/get methods cannot be found for
	 *             propertyName.
	 */
	public static <T> Animator createAnimator(int duration, Object object,
			String propertyName, Evaluator<?> evaluator, T... params) {
		PropertySetter ps = new PropertySetter(object, propertyName, evaluator,
				params);
		Animator animator = new Animator(duration, ps);
		return animator;
	}

	/**
	 * Constructor for a PropertySetter where the values the propert takes on
	 * during the animation are specified in a {@link KeyFrames} object.
	 * 
	 * @param object
	 *            the object whose property will be animated
	 * @param propertyName
	 *            the name of the property to be animated. For any propertyName
	 *            "foo" there must be an accessible "setFoo" method on the
	 *            object. If only one value is supplied in creating the
	 *            KeyValues for the keyFrames, the animation will also need a
	 *            "getFoo" method.
	 * @param keyFrames
	 *            the fractional times, values, and interpolation to be used in
	 *            calculating the values set on the object's property.
	 * @throws IllegalArgumentException
	 *             if appropriate set/get methods cannot be found for
	 *             propertyName.
	 */
	public PropertySetter(Object object, String propertyName,
			KeyFrames keyFrames) {
		this.object = object;
		this.propertyName = propertyName;
		this.keyFrames = keyFrames;
		try {
			setupMethodInfo();
		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException(
					"Bad property name ("
							+ propertyName
							+ "): could not find "
							+ "an appropriate setter or getter method for that property");
		}
	}

	/**
	 * Constructor for a PropertySetter where the values the propert takes on
	 * during the animation are specified in a {@link KeyFrames} object.
	 * 
	 * @param object
	 *            the object whose property will be animated
	 * @param propertyName
	 *            the name of the property to be animated. For any propertyName
	 *            "foo" there must be an accessible "setFoo" method on the
	 *            object. If only one value is supplied in params, the animation
	 *            will also need a "getFoo" method.
	 * @param params
	 *            the values that the object will take on during the animation.
	 *            Internally, a KeyFrames object will be created that will use
	 *            times that split the total duration evenly. Supplying only one
	 *            value for params implies that this is a "to" animation whose
	 *            intial value will be determined dynamically when the animation
	 *            starts.
	 * @throws IllegalArgumentException
	 *             if appropriate set/get methods cannot be found for
	 *             propertyName.
	 */
	public <T> PropertySetter(Object object, String propertyName, T... params) {
		this(object, propertyName, new KeyFrames(KeyValues.create(params)));
	}

	/**
	 * Constructor for a PropertySetter where the values the propert takes on
	 * during the animation are specified in a {@link KeyFrames} object.
	 * 
	 * @param object
	 *            the object whose property will be animated
	 * @param propertyName
	 *            the name of the property to be animated. For any propertyName
	 *            "foo" there must be an accessible "setFoo" method on the
	 *            object. If only one value is supplied in params, the animation
	 *            will also need a "getFoo" method.
	 * @param evaluator
	 *            KeyValues knows how to calculate intermediate values for many
	 *            built-in types, but if you want to supply values in types not
	 *            understood by KeyValues, you will need to supply your own
	 *            Evaluator.
	 * @param params
	 *            the values that the object will take on during the animation.
	 *            Internally, a KeyFrames object will be created that will use
	 *            times that split the total duration evenly. Supplying only one
	 *            value for params implies that this is a "to" animation whose
	 *            intial value will be determined dynamically when the animation
	 *            starts.
	 * @throws IllegalArgumentException
	 *             if appropriate set/get methods cannot be found for
	 *             propertyName.
	 */
	public <T> PropertySetter(Object object, String propertyName,
			Evaluator<?> evaluator, T... params) {
		this(object, propertyName, new KeyFrames(KeyValues.create(evaluator,
				params)));
	}

	/**
	 * Translates the property name used in the PropertyRange object into the
	 * appropriate Method in the Object to be modified. This uses standard
	 * JavaBean naming convention (e.g., propertyName would become
	 * setPropertyName).
	 * 
	 * @throws NoSuchMethodException
	 *             if there is no method on the object with the appropriate name
	 * @throws SecurityException
	 *             if the application does not have appropriate permissions to
	 *             request access to the Method
	 */
	private void setupMethodInfo() throws NoSuchMethodException {
		try {
			String firstChar = propertyName.substring(0, 1);
			String remainder = propertyName.substring(1);
			String propertySetterName = "set" + firstChar.toUpperCase()
					+ remainder;

			PropertyDescriptor prop = new PropertyDescriptor(propertyName,
					object.getClass(), null, propertySetterName);
			propertySetter = prop.getWriteMethod();
			if (isToAnimation()) {
				// Only need the getter for "to" animations
				String propertyGetterName = "get" + firstChar.toUpperCase()
						+ remainder;
				prop = new PropertyDescriptor(propertyName, object.getClass(),
						propertyGetterName, null);
				propertyGetter = prop.getReadMethod();
			}
		} catch (Exception e) {
			throw new NoSuchMethodException("Cannot find property methods: "
					+ e);
		}
	}

	//
	// TimingTargetAdapter overrides
	//

	/**
	 * Called by Animator to signal that the timer is about to start. The only
	 * operation performed in this method is setting an initial value for the
	 * animation if appropriate; this accounts for "to" animations, which need
	 * to start from the current value.
	 * <p>
	 * This method is not intended for use by application code.
	 */
	public void begin() {
		if (isToAnimation()) {
			try {
				setStartValue(propertyGetter.invoke(object));
			} catch (Exception e) {
				System.out.println("Problem setting start value on object "
						+ object + ": " + e);
			}
		}
	}

	/**
	 * Called from Animator to signal a timing event. This causes PropertySetter
	 * to invoke the property-setting method (as specified by the propertyName
	 * in the constructor) with the appropriate value of the property given the
	 * range of values in the KeyValues object and the fraction of the timing
	 * cycle that has elapsed.
	 * <p>
	 * This method is not intended for use by application code.
	 */
	public void timingEvent(float fraction) {
		try {
			setValue(object, propertySetter, fraction);
		} catch (Exception e) {
			System.out.println("Problem calling setValue in "
					+ "PropertySetter.timingEvent: " + e);
		}
	}

	private String getPropertyName() {
		return propertyName;
	}

	/**
	 * Called during begin() if this is a "to" animation, to set the start value
	 * of the animation to whatever the current value is.
	 */
	private void setStartValue(Object object) {
		keyFrames.getKeyValues().setStartValue(object);
	}

	/**
	 * Sets the appropriate value on the property given the current fraction
	 */
	private void setValue(Object object, Method method, float fraction) {
		try {
			method.invoke(object, keyFrames.getValue(fraction));
		} catch (Exception e) {
			System.out.println("Problem invoking method " + propertySetter
					+ " in object " + object + " in setValue" + e);
		}
	}

	/**
	 * Returns the type used in this property setter (defers to KeyFrames for
	 * this information).
	 */
	private Class getType() {
		return keyFrames.getType();
	}

	/**
	 * Utility method for determining whether this is a "to" animation (true if
	 * the first value is null).
	 */
	private boolean isToAnimation() {
		return (keyFrames.getKeyValues().isToAnimation());
	}

}
